AI项目实战(9)一站式代码开发 Agent

《AI Agent 实战》系列 · 一站式代码开发 Agent

Posted by Ryan on 2026-07-01
Estimated Reading Time 33 Minutes
Words 8.2k In Total
Viewed Times

主讲能力:复杂 Harness 闭环、测试驱动开发、自动修复循环
业务场景:用户提出一个功能需求,Agent 自动完成需求分析、接口设计、代码生成、测试编写、测试运行、失败修复与最终交付。
对应代码backend/projects/p06_code_dev/


8.1 老张的执念

8.1.1 业务背景

先讲个真事。

周二下午,后端老张接到产品一个需求:写个函数,算订单用完优惠券后还该付多少钱。需求听着不复杂,老张端着咖啡,半小时就把代码敲完了,屏幕上一行行整整齐齐,看着挺顺眼。可临提交前他心里发虚——这玩意儿到底对不对?于是老老实实补了几个测试,一跑,红了。改两行,再跑,又红。来回折腾到第六遍,屏幕上终于齐刷刷一片绿点,他长舒一口气,抬头一看,天都黑了。

老张这一下午其实在干嘛?就干了一件事:写代码、跑测试、看报错、改代码、再跑——一个不断逼近正确的闭环。

再看另一面。隔壁组小李用上了某个"AI 写代码"工具,让它生成分页查询函数。AI 吐出来的代码漂漂亮亮,有类型注解、有 docstring,小李扫了一眼,“嗯,看起来对”,直接提了 PR 合并上线。结果第二天线上就报了错——边界条件没处理,最后一页数据直接漏掉。AI 写的代码"看起来对",可没人真去验证过。

两个故事指向同一件事:代码写完不算完,验证过才算完

真实研发里,一个小功能从需求到上线,要走过需求澄清、技术设计、编码、测试、修复、再验证、提交说明这一长串。市面上不少 AI 代码工具只停在"帮我写一段代码"这一步,写得像模像样,却没人替你兜底验证。可生产开发真正需要的,是一个闭环系统:写完要能验证,验证失败要能修,修完还得再测——直到测试说"过了"为止。

本章的一站式代码开发 Agent 不追求替代整个研发团队,而是聚焦在中小型可自动化开发任务上:工具函数、API 端点、数据处理脚本、测试用例、简单的模块重构。说白了,就是让 Agent 把老张那一下午的活儿——写、测、改、再测——自己包圆了。

8.1.2 目标用户

用户 场景
后端工程师 快速生成工具函数/API/测试
技术负责人 将重复开发任务交给 Agent 初步完成
学习者 学习标准的测试驱动开发闭环

8.1.3 功能需求

  • FR-1:接收自然语言需求,先生成结构化需求分析。
  • FR-2:生成实现代码与测试代码。
  • FR-3:自动运行测试。
  • FR-4:若测试失败,读取相关文件、定位错误、修复代码、重新测试。
  • FR-5:最多自动修复 3 轮,超过后输出失败原因与人工建议。
  • FR-6:所有文件写入受控工作空间,不越权修改用户项目文件。

8.1.4 非功能需求

  • 工具执行有超时控制,防止测试卡死。
  • 文件写入限定在 backend/data/code_workspace/
  • 每次工具调用有结构化日志。
  • 自动生成代码需具备类型注解、docstring、测试用例。

8.2 画个样子:它该长啥样

8.2.1 功能流程

%%{init: {'theme':'base','flowchart':{'useMaxWidth':true,'htmlLabels':true}}}%%
graph TD
    U["用户需求"] --> A["需求分析"]
    A --> T["生成测试"]
    T --> C["生成实现代码"]
    C --> R["运行测试"]
    R -->|通过| Done["交付代码和报告"]
    R -->|失败| D["读取失败日志和文件"]
    D --> F["修复代码"]
    F --> R
    R -->|超过3轮仍失败| H["输出人工接管建议"]

    classDef n fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1
    classDef ok fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:#1b5e20
    classDef warn fill:#fff3e0,stroke:#fb8c00,stroke-width:2px,color:#e65100
    class U,A,T,C,R,D,F n
    class Done ok
    class H warn

8.2.2 用户交互样例

用户输入:

写一个 fibonacci(n) 函数,返回前 n 项斐波那契数列,并附带测试。

Agent 输出应包含:

  • fibonacci.py
  • test_fibonacci.py
  • 测试运行结果
  • 修复记录(如果有)
  • 最终使用说明

8.3 拆开看:怎么造出来

8.3.1 架构设计

%%{init: {'theme':'base','flowchart':{'useMaxWidth':true,'htmlLabels':true}}}%%
graph LR
    Agent["create_agent"] --> Tools["工具集合"]
    Tools --> W["write_file"]
    Tools --> Read["read_file"]
    Tools --> LS["list_workspace"]
    Tools --> Test["run_tests"]
    Tools --> Req["analyze_requirement"]
    Test --> Pytest["pytest 子进程"]
    W --> WS[("受控工作空间")]
    Read --> WS
    LS --> WS

    classDef a fill:#ede7f6,stroke:#5e35b1,stroke-width:2px,color:#311b92
    classDef t fill:#e0f7fa,stroke:#00acc1,stroke-width:2px,color:#006064
    classDef s fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:#1b5e20
    class Agent a
    class Tools,W,Read,LS,Test,Req t
    class Pytest,WS s

8.3.2 核心设计

很多人一上来会盯着模型本身较劲——“是不是换个更强的模型就行了?”。但本项目的关键不在模型,而在可控的工具边界。模型再强,也得通过工具动手;工具的边界画在哪,Agent 的能力天花板就在哪。我们给 Agent 配了四件趁手的家伙:

  1. write_file:只能写进受控工作空间,别处一概不许碰。
  2. read_file:读取时自动截断,防止一份大文件把上下文撑爆。
  3. run_tests:开独立子进程跑 pytest,外加 60 秒超时,慢了就掐。
  4. analyze_requirement:逼着 Agent 先做需求分析、再动笔写代码。

这四件工具合起来,就圈出了 Agent 的"活动范围"——它什么都能干,但只能在围栏里干。

8.3.3 为什么不是直接让模型写代码

这个问题乍一听有点傻——AI 不就是用来写代码的吗?让它直接写不就完了?

但你回头想想小李的故事。让模型直接吐一段代码,最大的问题不是写得丑,而是没有验证。代码生成出来的那一刻,没人知道它对不对——连模型自己都不知道,它只是"看起来"写得很顺。要是就这么交出去,等于把一个未经检验的答案直接塞进生产线。

生产级开发有一条铁律:别问模型"你写对了吗",要用测试证明它对。模型嘴上说"这段代码实现了斐波那契数列",这话一文不值;只有 pytest 跑出一排绿点,才算数。

所以本项目干了一件反直觉的事:把测试从"事后补一步"提升为整个闭环的中心

  • 先让 Agent 做需求分析,把接口和验收标准想清楚——磨刀不误砍柴工。
  • 再让它写测试(没错,先写测试)。
  • 然后才写实现代码。
  • 写完立刻跑测试。
  • 测试要是红了,就回去读代码、定位、修,再跑——如此循环,最多三轮。

这就是所谓的测试驱动闭环(TDD-flavored Harness)。它的灵魂就一句话:

测试是 Agent 行为的事实反馈,不是模型的自我感觉。

你大概会问:既然最后都要修,为什么不能一次写对,非要绕这个圈?

💡 顿悟时刻:因为模型本来就不是一次写对的。哪怕是最强的模型,碰上边界条件、异常输入、类型细节,照样会翻车。与其幻想"一次写对"然后祈祷不出事,不如大方承认它会错,干脆把"错了→改→再验"这件事机制化——让循环替你兜底。一次写对是运气,循环收敛才是工程。

你大概还会问:那为什么要先写测试?实现都还没有,测什么?

这正是 TDD 的精髓。先写测试,逼着 Agent 在动手编码前,就把"什么算对"想清楚——参数是什么、返回什么、边界在哪。测试一旦写好,实现就有了靶子,跑通的那一刻就是验收通过的那一刻。反过来,如果先写实现再补测试,人(和 Agent)很容易下意识地照着实现去凑测试,测了等于没测——自己考自己,当然满分。

⚠️ 避坑:先写测试还有个隐藏好处。Agent 改代码时,测试就是它的"安全网"——改完一跑,绿了说明没改坏,红了说明改出新问题了。没有这张网,Agent 每次修改都像在悬崖边蒙眼走路,改着改着就回不去了。

所以这一节真正回答的,不是"要不要让模型写代码",而是"怎么让模型写的代码可信"。答案就是:让它自己写、自己测、自己改,形成一个能自我纠错的闭环。下面 8.4 节,咱们就把这个闭环一行行地落地。


8.4 动手写:三层架构完整代码

本节给出项目六的完整实现代码。代码不是一锅乱炖,而是按职责分层堆起来的——从最底下的数据模型,一层层往上叠到对外接口。每一层只依赖它下面那一层,形成一个职责清晰、能独立测试、方便后续演进的开发闭环。核心代码都住在 backend/projects/p06_code_dev/ 目录里。

8.4.1 三层架构完整代码

项目按职责拆成六个文件,各司其职、单向依赖,谁也不回头找上家的麻烦,这样每一层都能单独拎出来测试和演进:

层次 文件 职责
模型层 models.py 数据模型、配置常量、领域对象
提示词层 prompts.py 系统提示词、模板
工具层 tools.py 文件读写、测试运行、需求分析工具
服务层 service.py 任务管理、流程编排、Agent 构建
项目层 project.py 项目注册、对外接口
入口 init.py 注册与 re-export

下面依次给出每个文件的完整代码。

models.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"""项目六:数据模型层。

定义代码开发 Agent 的领域模型、配置常量和数据结构。
"""
from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Optional

from core.config import DATA_DIR

# 工作空间根目录
WORKSPACE = DATA_DIR / "code_workspace"
WORKSPACE.mkdir(parents=True, exist_ok=True)

# 开发闭环配置
MAX_RETRY_ROUNDS = 3 # 最多自动修复 3 轮
TEST_TIMEOUT_SECONDS = 60 # 测试运行超时
MAX_FILE_CONTENT_LENGTH = 6000 # 文件读取最大长度


class TaskStatus(str, Enum):
"""任务状态。"""
PENDING = "pending"
ANALYZING = "analyzing"
CODING = "coding"
TESTING = "testing"
FIXING = "fixing"
SUCCESS = "success"
FAILED = "failed"


@dataclass
class CodeFile:
"""代码文件模型。"""
file_path: str
content: str
file_type: str = "python" # python, test, config
created_at: str = field(default="")


@dataclass
class TestResult:
"""测试结果模型。"""
success: bool
output: str
failed_files: list[str] = field(default_factory=list)
error_count: int = 0


@dataclass
class DevTask:
"""开发任务模型。"""
task_id: str
description: str
status: TaskStatus = TaskStatus.PENDING
files: list[CodeFile] = field(default_factory=list)
test_results: list[TestResult] = field(default_factory=list)
retry_count: int = 0
final_report: str = ""

def can_retry(self) -> bool:
"""判断是否还可以重试。"""
return self.retry_count < MAX_RETRY_ROUNDS

def add_file(self, file_path: str, content: str, file_type: str = "python") -> None:
"""添加代码文件。"""
self.files.append(CodeFile(
file_path=file_path,
content=content,
file_type=file_type,
))

def add_test_result(self, success: bool, output: str) -> None:
"""添加测试结果。"""
self.test_results.append(TestResult(
success=success,
output=output,
))

prompts.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""项目六:Prompt 层。

定义代码开发 Agent 的系统提示词和工具描述模板。
"""
from __future__ import annotations

# 主 Agent 系统提示词
SYSTEM_PROMPT = """你是全栈开发 Agent,负责从需求到代码的完整闭环。

## 核心能力
1. 需求分析:将自然语言需求转成结构化规格说明
2. 代码生成:生成符合规范的 Python 代码和测试
3. 测试运行:自动执行测试并收集结果
4. 错误修复:根据测试失败日志定位问题并修复

## 工作流程(严格按顺序)
1. **第一步**:调用 analyze_requirement 分析需求,明确接口和验收标准
2. **第二步**:基于分析结果,用 write_file 创建代码文件(先写测试,再写实现)
3. **第三步**:调用 run_tests 运行测试验证
4. **第四步**:如果测试失败:
- 调用 read_file 查看相关代码
- 定位问题根因
- 调用 write_file 修复代码
- 调用 run_tests 重新验证
5. 修复循环最多 3 轮,3 轮后仍失败则报告问题和你的分析
6. 全部通过后,汇报最终工作成果

## 编码规范
- Python 3.10+,使用类型注解
- 每个函数有 docstring,说明参数、返回值、异常
- 单文件不超过 300 行
- 测试和实现分开:test_xxx.py + xxx.py
- 使用 pytest 风格的测试用例
- 关键逻辑要有注释

## 文件命名约定
- 实现代码:{module_name}.py
- 测试代码:test_{module_name}.py
- 配置文件:config.py 或 settings.py

## 输出要求
- 每一步操作后简要说明做了什么
- 测试通过后列出所有创建的文件
- 如有修复,说明修改了什么和为什么
- 最终给出使用示例"""


# 需求分析工具的系统提示词
ANALYSIS_PROMPT = """你是需求分析师。请分析以下功能需求,输出结构化规格说明。

## 需要包含的内容
1. 功能描述:一句话概括这个功能做什么
2. 接口定义:函数签名、参数类型、返回值类型
3. 输入输出示例:至少 2 个示例
4. 边界条件:异常输入、空值、极限值的处理
5. 验收标准:什么样的结果算通过

## 输出格式
用 Markdown 格式,结构清晰,便于后续编码实现。"""


# 代码生成提示词模板
CODE_GEN_TEMPLATE = """根据以下需求生成高质量 Python 代码:

需求分析:
{analysis}

要求:
1. 类型注解完整
2. 有 docstring
3. 错误处理适当
4. 代码简洁可读

生成文件:{file_name}"""


# 测试生成提示词模板
TEST_GEN_TEMPLATE = """为以下代码生成 pytest 测试用例:

实现代码:
{code}

要求:
1. 正常情况测试
2. 边界条件测试
3. 异常情况测试
4. 测试用例有描述性名称

生成文件:test_{module_name}.py"""


# 错误修复提示词模板
FIX_PROMPT = """根据以下测试失败信息修复代码:

失败日志:
{error_log}

相关代码:
{code_content}

要求:
1. 定位问题根因
2. 给出修复方案
3. 保证不引入新问题
4. 保持原有接口不变"""

tools.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""项目六:工具层。

定义代码开发 Agent 使用的所有工具函数。
"""
from __future__ import annotations

import os
import subprocess
from typing import Any

from langchain.tools import tool

from core.config import get_settings
from core.logging_conf import get_logger

from .models import (
MAX_FILE_CONTENT_LENGTH,
TEST_TIMEOUT_SECONDS,
WORKSPACE,
)

logger = get_logger("p06.code_dev.tools")


@tool
def write_file(file_path: str, content: str) -> str:
"""将内容写入指定文件(在代码工作空间内)。

Args:
file_path: 相对于工作空间的路径,如 'app.py'、'utils/helpers.py'
content: 文件的完整内容

Returns:
操作结果描述
"""
try:
full = WORKSPACE / file_path
full.parent.mkdir(parents=True, exist_ok=True)
full.write_text(content, encoding="utf-8")
logger.info("写入文件: %s (%d 字节)", full, len(content))
return f"✅ 文件已保存: {file_path} ({len(content)} 字节)"
except Exception as e:
logger.error("写入文件失败: %s", e)
return f"❌ 写入失败: {e}"


@tool
def read_file(file_path: str) -> str:
"""读取工作空间中的文件内容。

Args:
file_path: 相对于工作空间的文件路径

Returns:
文件内容或错误信息
"""
full = WORKSPACE / file_path
try:
content = full.read_text(encoding="utf-8")
if len(content) > MAX_FILE_CONTENT_LENGTH:
content = content[:MAX_FILE_CONTENT_LENGTH] + "\n... (内容过长,已截断)"
return content
except FileNotFoundError:
return f"❌ 文件不存在: {file_path}"
except Exception as e:
logger.error("读取文件失败: %s", e)
return f"❌ 读取失败: {e}"


@tool
def list_workspace() -> str:
"""列出代码工作空间中所有文件。

Returns:
工作空间文件列表
"""
try:
files = []
for p in sorted(WORKSPACE.rglob("*")):
if p.is_file() and ".git" not in str(p) and "__pycache__" not in str(p):
rel = p.relative_to(WORKSPACE)
size = p.stat().st_size
files.append(f" {rel} ({size} 字节)")
if not files:
return "📂 工作空间为空,尚无文件。"
result = "📂 工作空间文件:\n" + "\n".join(files[:50])
if len(files) > 50:
result += f"\n ... 还有 {len(files) - 50} 个文件"
return result
except Exception as e:
logger.error("列出文件失败: %s", e)
return f"❌ 列出文件失败: {e}"


@tool
def run_tests(test_path: str = ".") -> str:
"""运行指定路径的 Python 测试。

Args:
test_path: 测试目录或文件路径(相对于工作空间),默认为当前目录

Returns:
测试运行结果
"""
full = WORKSPACE / test_path
try:
result = subprocess.run(
["python", "-m", "pytest", str(full), "-x", "--tb=short", "-q"],
capture_output=True,
text=True,
timeout=TEST_TIMEOUT_SECONDS,
cwd=str(WORKSPACE),
env={**os.environ, "PYTHONPATH": str(WORKSPACE)},
)
output = result.stdout
if len(output) > MAX_FILE_CONTENT_LENGTH:
output = output[-MAX_FILE_CONTENT_LENGTH:] + "\n... (输出过长,已截断末尾)"

if not output.strip():
output = result.stderr[-2000:] if len(result.stderr) > 2000 else result.stderr

if result.returncode == 0:
return f"✅ 测试通过!\n\n{output or '无输出'}"
else:
return f"❌ 测试失败(退出码: {result.returncode})\n\n{output or '无输出'}"
except subprocess.TimeoutExpired:
return f"⏱️ 测试运行超时({TEST_TIMEOUT_SECONDS} 秒),请检查是否有死循环或慢速测试"
except Exception as e:
logger.error("测试运行失败: %s", e)
return f"❌ 测试运行失败: {e}"


@tool
def analyze_requirement(description: str) -> str:
"""分析功能需求,输出结构化规格说明。

包括:接口定义(函数签名)、输入输出类型、边界条件、验收标准。

Args:
description: 用户的功能需求描述

Returns:
结构化的需求分析
"""
return (
f"📋 需求分析\n"
f"原始描述:{description}\n\n"
f"(Agent 将基于此分析进行接口设计和编码实现)\n\n"
f"建议分析方向:\n"
f"1. 功能概述:这个函数/模块主要做什么?\n"
f"2. 输入参数:需要哪些参数?类型是什么?\n"
f"3. 返回值:返回什么数据?结构如何?\n"
f"4. 边界条件:空值、异常值、极限值如何处理?\n"
f"5. 验收标准:什么情况算功能正确?"
)


def get_all_tools() -> list[Any]:
"""获取所有可用工具列表。

Returns:
工具对象列表
"""
return [
analyze_requirement,
write_file,
read_file,
list_workspace,
run_tests,
]

service.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
"""项目六:服务层。

封装代码开发 Agent 的核心业务逻辑,包括任务管理、流程编排等。
"""
from __future__ import annotations

from typing import Any

from core import build_chat_model
from core.logging_conf import get_logger

from .models import DevTask, TaskStatus
from .prompts import SYSTEM_PROMPT
from .tools import get_all_tools

logger = get_logger("p06.code_dev.service")


class CodeDevService:
"""代码开发服务类。

封装代码开发 Agent 的核心业务逻辑,包括:
- 创建开发任务
- 跟踪任务状态
- 管理开发闭环
- 生成最终报告
"""

def __init__(self) -> None:
self._tasks: dict[str, DevTask] = {}
self._agent: Any | None = None

def create_task(self, description: str, task_id: str | None = None) -> DevTask:
"""创建新的开发任务。

Args:
description: 功能需求描述
task_id: 可选的任务 ID,自动生成

Returns:
任务对象
"""
import uuid

if task_id is None:
task_id = f"dev_{uuid.uuid4().hex[:8]}"

task = DevTask(
task_id=task_id,
description=description,
status=TaskStatus.PENDING,
)
self._tasks[task_id] = task
logger.info("创建开发任务: %s", task_id)
return task

def get_task(self, task_id: str) -> DevTask | None:
"""获取任务。

Args:
task_id: 任务 ID

Returns:
任务对象或 None
"""
return self._tasks.get(task_id)

def update_task_status(self, task_id: str, status: TaskStatus) -> bool:
"""更新任务状态。

Args:
task_id: 任务 ID
status: 新状态

Returns:
是否成功
"""
task = self.get_task(task_id)
if task is None:
logger.warning("任务不存在: %s", task_id)
return False
task.status = status
logger.info("任务 %s 状态更新为: %s", task_id, status)
return True

def increment_retry(self, task_id: str) -> bool:
"""增加任务重试计数。

Args:
task_id: 任务 ID

Returns:
是否成功(达到最大重试次数时返回 False)
"""
task = self.get_task(task_id)
if task is None:
return False
if not task.can_retry():
logger.warning("任务 %s 已达到最大重试次数", task_id)
return False
task.retry_count += 1
return True

def generate_final_report(self, task_id: str) -> str:
"""生成任务最终报告。

Args:
task_id: 任务 ID

Returns:
报告内容
"""
task = self.get_task(task_id)
if task is None:
return "任务不存在"

lines = [
"# 开发任务完成报告",
"",
f"**任务 ID**: {task.task_id}",
f"**需求描述**: {task.description}",
f"**最终状态**: {task.status.value}",
"",
"## 创建的文件",
"",
]

if task.files:
for f in task.files:
lines.append(f"- `{f.file_path}` ({f.file_type})")
else:
lines.append("(无文件创建记录)")

lines.extend([
"",
"## 测试结果",
"",
])

if task.test_results:
for i, result in enumerate(task.test_results, 1):
status = "✅ 通过" if result.success else "❌ 失败"
lines.append(f"第 {i} 轮: {status}")
else:
lines.append("(无测试运行记录)")

if task.retry_count > 0:
lines.extend([
"",
"## 修复记录",
"",
f"自动修复 {task.retry_count} 轮",
])

lines.extend([
"",
"## 使用说明",
"",
"所有文件已保存到工作空间目录。",
"如需进一步修改,请提供新的需求描述。",
])

report = "\n".join(lines)
task.final_report = report
return report

def build_agent(self) -> Any:
"""构建代码开发 Agent。

Returns:
LangChain Agent 对象
"""
if self._agent is not None:
return self._agent

from langchain.agents import create_agent

self._agent = create_agent(
model=build_chat_model(),
tools=get_all_tools(),
system_prompt=SYSTEM_PROMPT,
)
return self._agent

def run_dev_task(self, description: str) -> str:
"""运行完整的开发任务流程。

Args:
description: 功能需求描述

Returns:
任务执行结果
"""
task = self.create_task(description)
self.update_task_status(task.task_id, TaskStatus.ANALYZING)

agent = self.build_agent()

try:
result = agent.run(description)
self.update_task_status(task.task_id, TaskStatus.SUCCESS)
return result
except Exception as e:
logger.error("开发任务执行失败: %s", e)
self.update_task_status(task.task_id, TaskStatus.FAILED)
return f"开发任务执行失败: {e}"


# 全局服务实例
_service: CodeDevService | None = None


def get_service() -> CodeDevService:
"""获取代码开发服务实例。

Returns:
CodeDevService 单例
"""
global _service
if _service is None:
_service = CodeDevService()
return _service

project.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
"""项目六:项目定义层。

定义项目注册和对外暴露的接口。
"""
from __future__ import annotations

from typing import Any

from core import BaseProject, registry

from .service import get_service
from .tools import get_all_tools


class CodeDevProject(BaseProject):
"""一站式代码开发 Agent 项目。

主讲能力:复杂 Harness 闭环、测试驱动开发、自动修复循环

业务场景:用户提出一个功能需求,Agent 自动完成需求分析、接口设计、
代码生成、测试编写、测试运行、失败修复与最终交付。
"""

id = "p06_code_dev"
name = "一站式代码开发 Agent"
description = "需求→设计→编码→测试→修复→验证,完整开发闭环。"
capabilities = ["Harness", "Tool", "Test-Driven"]

def build_agent(self) -> Any:
"""构建 Agent 实例。

Returns:
LangChain Agent 对象
"""
service = get_service()
return service.build_agent()

def run(self, message: str) -> str:
"""运行项目任务。

Args:
message: 用户输入的需求描述

Returns:
Agent 响应
"""
service = get_service()
return service.run_dev_task(message)


# 项目实例
project = CodeDevProject()

# 对外暴露的快捷函数
run = project.run
build_agent = project.build_agent
get_tools = get_all_tools

__all__ = [
"CodeDevProject",
"project",
"run",
"build_agent",
"get_tools",
]

init.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""项目六:一站式代码开发 Agent(复杂 Harness 闭环)

主讲能力:复杂 Harness 闭环——需求分析 → 代码生成 → 测试验证 → 修复 → 再测试

业务场景:用户描述功能需求,Agent 自动完成完整开发闭环:
分析需求 → 设计接口 → 生成代码 → 编写测试 → 运行测试 → 根据失败修复 → 重新验证

生产级特性:
- StateGraph 驱动的开发闭环(非简单一问一答)
- 需求分析和接口设计先于编码(避免返工)
- 测试驱动:先生成测试用例,再生成实现代码
- 自动修复循环:测试失败 → 分析原因 → 修复 → 重跑,最多 3 轮
- Git 友好:每次修改生成清晰的 commit message
- 文件操作安全:不覆盖用户未授权的文件

架构分层:
- models.py: 数据模型、配置常量
- prompts.py: 系统提示词、模板
- tools.py: 工具函数
- service.py: 业务逻辑、Agent 编排
- project.py: 项目定义、对外接口
"""
from __future__ import annotations

from core import registry

# 导出 models 层
from .models import (
CodeFile,
DevTask,
MAX_FILE_CONTENT_LENGTH,
MAX_RETRY_ROUNDS,
TaskStatus,
TestResult,
TEST_TIMEOUT_SECONDS,
WORKSPACE,
)

# 导出 prompts 层
from .prompts import (
ANALYSIS_PROMPT,
CODE_GEN_TEMPLATE,
FIX_PROMPT,
SYSTEM_PROMPT,
TEST_GEN_TEMPLATE,
)

# 导出 tools 层(保持向后兼容)
from .tools import (
analyze_requirement,
get_all_tools,
list_workspace,
read_file,
run_tests,
write_file,
)

# 导出 service 层
from .service import CodeDevService, get_service

# 导出 project 层
from .project import (
CodeDevProject,
build_agent,
get_tools,
project,
run,
)

# 注册项目到全局注册表
registry.register(project)

__all__ = [
# models
"WORKSPACE",
"MAX_RETRY_ROUNDS",
"TEST_TIMEOUT_SECONDS",
"MAX_FILE_CONTENT_LENGTH",
"TaskStatus",
"CodeFile",
"TestResult",
"DevTask",
# prompts
"SYSTEM_PROMPT",
"ANALYSIS_PROMPT",
"CODE_GEN_TEMPLATE",
"TEST_GEN_TEMPLATE",
"FIX_PROMPT",
# tools
"write_file",
"read_file",
"list_workspace",
"run_tests",
"analyze_requirement",
"get_all_tools",
# service
"CodeDevService",
"get_service",
# project
"CodeDevProject",
"project",
"run",
"build_agent",
"get_tools",
]

8.4.2 核心代码讲解

上面这六个文件看着平铺直叙,其实每一处都藏着一条生产级的设计决策。咱们挨个扒开来看。

受控工作空间的路径安全设计

先说一个最容易翻车的地方:给 Agent 文件写权限。给少了它干不了活,给多了它能把你整个项目改花。怎么两全?

models.py 在模块一加载时就建好 WORKSPACE = DATA_DIR / "code_workspace" 这个根目录,往后所有文件操作都从这里出发。tools.py 里的 write_fileread_filerun_tests,无一例外都用 WORKSPACE / file_path 来拼路径——Agent 哪怕想耍小聪明传个 ../../etc/passwd 之类的路径,也翻不出这五指山。write_file 还顺手调了 full.parent.mkdir(parents=True, exist_ok=True),自动把子目录建好,让 Agent 能自由组织多文件工程,又不会捅破隔离边界。

一句话总结这个设计:给 Agent 文件权限,但不给它无边界的权限

⚠️ 避坑:工作空间隔离不是可选项,是底线。一旦 Agent 能直接写用户的项目文件,一个 write_file 调用就可能覆盖掉线上配置或别人未提交的代码。把它关进 code_workspace 这个笼子里,哪怕它写崩了,崩的也只是这个一次性沙盒,拍拍屁股重来就行。

测试驱动的 Harness 闭环

接下来是整章的灵魂。run_tests 没有图省事在当前进程里 import 被测代码,而是郑重其事地用 subprocess.run 拉起一个独立子进程跑 pytest,还专门设了 cwd=str(WORKSPACE)PYTHONPATH=str(WORKSPACE)。这么折腾图什么?图的是彻底隔离——被测代码里万一有什么副作用(改全局变量、注册钩子、甚至死循环),也污染不到 Agent 主进程。-x --tb=short -q 这串参数,意思是"一失败就停、traceback 给短点、输出别啰嗦",正好喂给 Agent 去定位问题。

测试的退出码和输出,就是 Harness 的"事实反馈"——模型说对不算数,测试通过才算数。这正是 8.3 节那句"为什么不是直接让模型写代码"的工程落地。

💡 顿悟时刻:你会发现,整个闭环里模型其实没有最终决定权,测试才有。模型可以天花乱坠地解释自己的代码多优雅,但只要 pytest 退出码非零,它就得乖乖回去改。测试是唯一不带偏见的裁判。

自动修复循环(最多 3 轮)

光会跑测试还不够,跑挂了得会修。models.py 里定了个 MAX_RETRY_ROUNDS = 3DevTask.can_retry() 拿它来判断还能不能再试;service.pyincrement_retry 一旦发现到顶了,就返回 False 并敲一行警告日志。光有代码约束还不够,SYSTEM_PROMPT 把规矩也写进了提示词:测试失败后,老老实实按"读文件 → 定位根因 → 修复 → 重跑"的顺序来,最多三轮,三轮还过不了就如实汇报问题和分析——既不无限死磕,也不装作成功。

为什么是 3 轮,不是 1 轮也不是 30 轮?这是个经验值:1 轮太苛刻,模型第一次难免手生;30 轮太放纵,容易在同一个坑里反复横跳烧 token。3 轮给足纠错空间,又逼着 Agent 在有限步骤里收敛。

配置常量管"能不能",提示词管"该不该"——双重约束,把"高级工程师的工作纪律"固化进了系统。

三层架构的解耦优势

本项目在经典"模型—工具—服务"三层架构上又细切了一刀,变成六个文件,但单向依赖的规矩没变:models.py 只管数据结构和常量,不沾任何业务;prompts.py 只放纯字符串提示词;tools.py 依赖 models 给的常量;service.py 调度前三层来编排任务、构建 Agent;project.py 只负责把服务包成统一的 BaseProject 接口;__init__.py 收尾做聚合导出和注册。

这么分有什么好?每一层都能被单独替换或单独测。换数据模型不碍着工具,换工具不碍着提示词,哪天想换 LLM 框架,只动 service.build_agent 一个函数就够。分层还顺带让单元测试变得轻松——每一层都能单独 mock,不必非得连上真实 LLM 才跑得起来。

错误降级和日志设计

最后说一处不起眼但救命的设计。每个工具都把核心逻辑裹在 try/except 里,出了岔子不抛异常,而是带个 ❌ 前缀转成友好字符串返回给 Agent。这一手至关重要:闭环不中断,Agent 才能"看见"错误并据此去修,而不是整个流程一崩了之。

各处还埋了防溢出的小心机:read_file 把超长内容截到 MAX_FILE_CONTENT_LENGTH,防上下文爆炸;run_tests 从末尾截断超长输出(错误信息最关键的尾巴得留着),还单独接住 TimeoutExpired,免得一个死循环测试把整个 Agent 拖垮。所有关键操作都通过 get_logger 吐结构化日志(写了多少字节、状态怎么变、重试到第几次),既方便人肉调试,又不会污染喂给 Agent 的内容。最外层 service.run_dev_task 还兜了底,万一真出意外,把任务状态降级成 FAILED 并返回一句人话,保证调用方拿到的永远是个字符串,而不是一坨栈崩溃。

⚠️ 避坑:子进程超时这条线必须拉紧。测试代码是 Agent 现写的,质量没保证——万一里面藏个 while True,没有 timeout 兜着,整个 Agent 就会卡死在那儿再也回不来。TEST_TIMEOUT_SECONDS = 60 就是那根救命的保险丝。


8.5 跑一跑:它真的行吗

光说不练假把式,Agent 自己写的代码要测,项目本身也得测。离线测试覆盖这几块:

  • 文件写入与读取
  • 工作空间列表
  • 需求分析工具
  • 集成测试(需 API Key)

运行方式:

1
2
cd backend
pytest projects/p06_code_dev/test_agent.py -v

8.6 送上线:让它上班

该项目已经注册到统一后端和前端管理台,部署方式和前几章如出一辙:

1
2
uvicorn main:app --reload --port 8000
cd frontend && npm run dev

起好服务,到前端选「一站式代码开发 Agent」,把开发需求丢进去,就能眼睁睁看着它一步步调用工具、跑测试、看反馈——闭环是怎么转起来的,一清二楚。


8.7 回头看:学到了什么

走到这儿,我们把 Agent 从"能回答"推进到了"能交付代码"。回过头看,老张那一下午来回折腾的循环,被我们写成了一段会自我纠错的程序。几条关键经验,值得带走:

  • 不要信模型的自我声明,用测试验证——嘴上说对不算数,绿点才算数。
  • 不要给 Agent 无边界的文件权限,用工作空间隔离——能干活的笼子,才是好笼子。
  • 不要让失败停在原地,用闭环让它修复——错了不可怕,错了能改回来才叫工程。

一句话:让 Agent 自己写、自己测、自己修,把"信任"建立在"验证"之上,而不是建立在"看起来对"之上。

下一章,我们迈入 Multi-Agent 阶段,让多个智能体组团,搭起一支内容创作团队。


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !